﻿using SharpDepend.Datatypes;
using SharpDepend.Storage;
using SoundEngine3;
using SoundEngine3.AudioContent;
using SoundEngine3.AudioProcessing;
using SoundEngine3.DspPlugins.Meta;
using SoundEngine3.Internal;
using SoundEngine3.MIDI.Meta;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ZquenceStudio3.Core.Storage;
using ZquenceStudio3Host.DspPlugins.Synthesizers.Nodesizer.Engine.Modules.Container;

namespace ZquenceStudio3Host.DspPlugins.Synthesizers.Nodesizer.Engine
{
    public static class ModulesRegistration
    {
        public struct ModuleInfo : IComparable<ModuleInfo>
        {
            public Type ModuleType;

            public string Display;

            public int CompareTo(ModuleInfo other)
            {
                return Display.CompareTo(other.Display);
            }
        }
        
        private static Dictionary<string, ModuleInfo> mTypes = new Dictionary<string, ModuleInfo>();

        public static IEnumerable<ModuleInfo> Modules { get { return mTypes.Values; } }

        public static void RegisterModule<T>() where T : BaseModule
        {
            Type typeT = typeof(T);
            
            if (!mTypes.ContainsKey(typeT.Name))
            {
                ModuleInfo info;
                info.ModuleType = typeT;
                info.Display = GetName(typeT);

                mTypes.Add(typeT.Name, info);
            }
        }

        internal static string GetName(Type module)
        {
            var customAttributes = (ModuleDisplayAttribute[])module.GetCustomAttributes(typeof(ModuleDisplayAttribute), true);
            if (customAttributes.Length > 0)
            {
                var myAttribute = customAttributes[0];

                return myAttribute.Display;
            }
            else
            {
                return module.Name;
            }
        }

        public static BaseModule CreateInstance(Type type, ModuleManager manager)
        {
            BaseModule module = (BaseModule)Activator.CreateInstance(type, manager);

            module.ChangeSampleRate(manager.SoundEngineContext.SampleRate);
            module.ChangeBufferSize(manager.SoundEngineContext.BufferSize);
            module.ChangeOutputChannels(manager.SoundEngineContext.OutputChannels);

            return module;
        }
        public static BaseModule CreateInstance(string moduleName, ModuleManager manager)
        {
            if (moduleName == "MultiplyModlue")
            {
                moduleName = "MultiplyModule";
            }
            Type type = mTypes[moduleName].ModuleType;
            
            return CreateInstance(type, manager);
        }
    }
    
    [AttributeUsage(AttributeTargets.Class)]
    public class ModuleDisplayAttribute : Attribute
    {
        public string Display { get; private set; }

        public ModuleDisplayAttribute(string display)
        {
            Display = display;
        }
    }

    /// <summary>
    /// Sample in.
    /// Sample out.
    /// 
    /// Parameters.
    /// </summary>
    public abstract class BaseModule : ParameterizedAudioInstance, IAudioProcessor
    {
        public const IOType NO_TYPE = (IOType)(-1);

        private BaseModule mParentModule;
        private List<ModuleInput> mInputs = new List<ModuleInput>();
        private List<ModuleOutput> mOutputs = new List<ModuleOutput>();
        private List<InputModuleParameter> mInputModuleParameters = new List<InputModuleParameter>();

        private BitVector8 mBits;

        internal bool _IsRenderInTrackArea
        {
            get
            {
                return this.mBits.Value1;
            }
            set
            {
                this.mBits.Value1 = value;
            }
        }

        internal bool Processed
        {
            get
            {
                return this.mBits.Value0;
            }
            private set
            {
                this.mBits.Value0 = value;
            }
        }

        private static int mModuleIONextID;

        internal ModuleManager Manager { get; private set; }

        public BaseModule ParentModule
        {
            get { return mParentModule; }
            set
            {
                mParentModule = value;
            }
        }

        public bool IsSuperGroupOf(BaseModule superGroupOf, out BaseModule lastSuper)
        {
            lastSuper = null;

            if (ParentModule == superGroupOf)
            {
                lastSuper = this;
                return true;
            }

            // Iss not super group of...
            if (ParentModule == null)
            {
                return false;
            }
            
            if (ParentModule.IsSuperGroupOf(superGroupOf, out lastSuper))
            {
                return true;
            }

            return false;
        }

        internal void SetProcessed(bool processed)
        {
            Processed = processed;

            if (!processed)
            {
                foreach (var output in Outputs)
                {
                    if (output.Type == IOType.Midi)
                    {
                        output.MidiBuffer.Clear();
                        output.AudioBuffer.Clear();
                    }

                    output.Processed();
                }

                foreach (var parameter in mInputModuleParameters)
                {
                    parameter.Process();
                }
            }
        }

        internal void ResetupSwitchableInputAndOutputs(IOType type)
        {
            HashSet<ModuleIO> processed = new HashSet<ModuleIO>();

            ResetupSwitchableInputAndOutputs(processed, type);
        }

        public static void SafeValue(ref double value)
        {
            if (value < 100000.0 && value > -100000.0)
            {
                return;
            }
#pragma warning disable CS1718 // Check NAN.
            else if (value != value)
#pragma warning restore CS1718 // Check NAN.
            {
                value = 0;
            }
            else if (value > 100000.0)
            {
                value = 100000.0;
            }
            else if (value < -100000.0)
            {
                value = -100000.0;
            }
        }

        private void ResetupSwitchableInputAndOutputs(HashSet<ModuleIO> processed, IOType type)
        {
            // Only if type is an Audio or Waveform.
            if (type == IOType.Audio || type == IOType.Waveform || type == IOType.AudioOrWaveform)
            {
                // Inputs.
                foreach (var input in mInputs)
                {
                    if (!processed.Contains(input))
                    {
                        processed.Add(input);
                        input.ResetupSwitchable(type);

                        // Deep setup.
                        if (input.IsConnected && input.FromOutput.Type == IOType.AudioOrWaveform)
                        {
                            input.FromOutput.Module.ResetupSwitchableInputAndOutputs(processed, type);
                        }
                    }
                }

                // Outputs.
                foreach (var output in mOutputs)
                {
                    if (!processed.Contains(output))
                    {
                        processed.Add(output);
                        output.ResetupSwitchable(type);

                        // Deep setup.
                        foreach (var connectedInput in output.ConnectedInputs)
                        {
                            if (connectedInput.Type == IOType.AudioOrWaveform)
                            {
                                connectedInput.Module.ResetupSwitchableInputAndOutputs(processed, type);
                            }
                        }
                    }
                }
            }
        }

        internal bool IsConnectedToNonSwitchable
        {
            get
            {
                return _IsConnectedToNonSwitchable(new HashSet<ModuleIO>());
            }
        }

        private bool _IsConnectedToNonSwitchable(HashSet<ModuleIO> inputOutputs)
        {
            foreach (var input in mInputs)
            {
                if (input.IsConnected && !inputOutputs.Contains(input))
                {
                    inputOutputs.Add(input);

                    if (input.FromOutput.Type != IOType.AudioOrWaveform)
                    {
                        return true;
                    }

                    if(input.FromOutput.Module._IsConnectedToNonSwitchable(inputOutputs))
                    {
                        return true;
                    }
                }
            }

            foreach (var output in mOutputs)
            {
                if (output.IsConnected && !inputOutputs.Contains(output))
                {
                    inputOutputs.Add(output);

                    foreach (var connectedInput in output.ConnectedInputs)
                    {
                        if (connectedInput.Type != IOType.AudioOrWaveform)
                        {
                            return true;
                        }

                        if (connectedInput.Module._IsConnectedToNonSwitchable(inputOutputs))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }


        /// <summary>
        /// Get or set the gui position.
        /// </summary>
        public Vector4 GuiPosition;

        /// <summary>
        /// Only call this if you set the Manager property manually in the subclass constructor.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="soundEngine"></param>
        protected BaseModule(SoundEngineContext soundEngine, UniqueIdentityManager<ParameterProperties> uniqueParameters = null)
            : base(soundEngine, uniqueParameters)
        {
            Name = ModulesRegistration.GetName(GetType());
            Inputs = new ReadOnlyCollection<ModuleInput>(mInputs);
            Outputs = new ReadOnlyCollection<ModuleOutput>(mOutputs);
        }

        public BaseModule(ModuleManager manager)
            : this(manager.SoundEngineContext, manager.mUniqueParameters)
        {
            Manager = manager;
        }

        protected internal void SetManager(ModuleManager manager)
        {
            Manager = manager;
        }

        public virtual void OnStoped()
        {

        }

        protected void AddModuleParameter(InputModuleParameter parameter)
        {
            if (parameter == null)
            {
                throw new ArgumentNullException(nameof(parameter));
            }
            if (parameter.InternalInputModule == null)
            {
                throw new ArgumentNullException(nameof(parameter.InternalInputModule));
            }

            AddInput(parameter.InternalInputModule);

            AddParameter(parameter);

            mInputModuleParameters.Add(parameter);
        }

        protected void RemoveModuleParameter(InputModuleParameter parameter)
        {
            if (parameter == null)
            {
                throw new ArgumentNullException(nameof(parameter));
            }
            if (parameter.InternalInputModule == null)
            {
                throw new ArgumentNullException(nameof(parameter.InternalInputModule));
            }

            RemoveInput(parameter.InternalInputModule);

            RemoveParameter(parameter);

            mInputModuleParameters.Remove(parameter);
        }

        public virtual void ChangeBufferSize(int bufferSize)
        {

        }

        /// <summary>
        /// Calls at the end when all modules has changed the buffer size. Use this when to interact with other modules and their input/outputs.
        /// </summary>
        /// <param name="bufferSize">New buffer size</param>
        public virtual void BufferSizeChanged(int bufferSize)
        {

        }

        public virtual void ChangeSampleRate(double sampleRate)
        {

        }

        public virtual void ChangeInputChannels(int channels)
        {

        }

        public virtual void ChangeOutputChannels(int channels)
        {

        }

        void IAudioProcessor.Process(BaseAudioBuffer input, BaseAudioBuffer output)
        {
            throw new NotSupportedException();
        }

        void IAudioProcessor.Process(BaseAudioBuffer buffer)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Increasing with one number.
        /// </summary>
        private static long mTemporaryCloneIdNext;

        /// <summary>
        /// This ID is generated everytime it is cloned.
        /// </summary>
        private long mTemporaryCloneId = -1;

        internal long GetTemporaryCloneId()
        {
            return mTemporaryCloneId;
        }

        /// <summary>
        /// Clone module, inclusive input connection and parameter values.
        /// <para>Not output connectins due it will be coflicts with the original module.</para>
        /// </summary>
        /// <param name="manager">ModuleManager.</param>
        /// <returns>Cloned module.</returns>
        public BaseModule Clone(ModuleManager manager)
        {
            BaseModule cloned = Clone(manager, 0);

            cloned.ImitateConnection(manager, this);
            
            return cloned;
        }

        /// <summary>
        /// Clone module, inclusive input connection and parameter values.
        /// <para>Not output connectins due it will be coflicts with the original module.</para>
        /// </summary>
        /// <param name="manager">ModuleManager.</param>
        /// <returns>Cloned module.</returns>
        /// <param name="deep">Module deep.</param>
        private BaseModule Clone(ModuleManager manager, int deep)
        {
            //BaseModule cloned = (BaseModule)Activator.CreateInstance(this.GetType(), manager);
            BaseModule cloned = ModulesRegistration.CreateInstance(this.GetType(), manager);

            cloned.Name = Name;
            cloned.GuiPosition = GuiPosition;
            cloned.ParentModule = ParentModule;

            // If this module is a group.
            if (this is GroupModule)
            {
                GroupModule group = this as GroupModule;

                foreach (var subModule in group.SubModules)
                {
                    BaseModule clonedSubModule = subModule.Clone(manager, deep);

                    (cloned as GroupModule).AddSubModule(clonedSubModule);
                }
            }
            
            // Copy parameter values.
            for (int i = 0; i < Parameters.Count; i++)
            {
                ParameterProperties parameter = Parameters[i];
                ParameterProperties clonedParameter = cloned.Parameters[i];

                if (clonedParameter is ModuleParameter)
                {
                    (clonedParameter as ModuleParameter).InternallyVisible = (parameter as ModuleParameter).InternallyVisible;
                }
                if (clonedParameter is InputModuleParameter)
                {
                    (clonedParameter as InputModuleParameter).OnBufferSizeChanged(SoundEngineContext.BufferSize);
                }

                clonedParameter.MinValue = parameter.MinValue;
                clonedParameter.MaxValue = parameter.MaxValue;
                clonedParameter.RawValue = parameter.RawValue;
                clonedParameter.Label = parameter.Label;
            }

            // Copy attributes for inputs.
            for (int i = 0; i < Inputs.Count; i++)
            {
                cloned.Inputs[i].InternallyVisible = Inputs[i].InternallyVisible;
                cloned.Inputs[i].SwitchedType = Inputs[i].SwitchedType;
            }

            // Copy attributes for outputs.
            for (int i = 0; i < Outputs.Count; i++)
            {
                cloned.Outputs[i].InternallyVisible = Outputs[i].InternallyVisible;
                cloned.Outputs[i].SwitchedType = Outputs[i].SwitchedType;
            }

            CustomNode node = new CustomNode();
            this.SetData(node);
            cloned.GetData(node);

            ++deep;

            return cloned;
        }

        /// <summary>
        /// Will imitate the input and output connection for the module and it's sub modules (if GroupModule) to be
        /// the sme as the module from the parameter.
        /// </summary>
        /// <param name="manager">ModuleManager.</param>
        /// <param name="module">Moudle to imitate the connnection from.</param>
        public void ImitateConnection(ModuleManager manager, BaseModule module)
        {
            GenerateTemporaryIDs(module, this);

            _ImitateConnection(manager, module);

            ClearAllTemporaryIDs();
            module.ClearAllTemporaryIDs();
        }

        public virtual void SetData(CustomNode data)
        {
        }

        public virtual void GetData(CustomNode data)
        {
        }

        /// <summary>
        /// Will imitate the input and output connection for the module and it's sub modules (if GroupModule) to be
        /// the sme as the module from the parameter.
        /// </summary>
        /// <param name="manager">ModuleManager.</param>
        /// <param name="module">Moudle to imitate the connnection from.</param>
        private void _ImitateConnection(ModuleManager manager, BaseModule module)
        {
            if (module == null)
            {
                throw new ArgumentNullException(nameof(module));
            }
            if (this == module)
            {
                throw new Exception("Can not imitate connection with the same module.");
            }
            if (this.GetType() != module.GetType())
            {
                throw new Exception("Can not imitate connection with a module that is another type.");
            }

            // If this module is a group.
            if (this is GroupModule)
            {
                GroupModule group = this as GroupModule;

                for (int i = 0; i < group.SubModules.Count; i++)
                {
                    BaseModule subModule = group.SubModules[i];
                    BaseModule subModule2 = (module as GroupModule).SubModules[i];

                    subModule._ImitateConnection(manager, subModule2);
                }
            }

            // Copy inputs connections.
            for (int i = 0; i < module.Inputs.Count; i++)
            {
                ModuleInput imitatedFromInput = module.Inputs[i];
                ModuleInput myInput = this.Inputs[i];

                if (imitatedFromInput.IsConnected)
                {
                    // Get the module from the output.
                    ModuleOutput imitatedFromOutput = imitatedFromInput.FromOutput;
                    BaseModule im = imitatedFromOutput.Module;
                    int outputIndex = im.Outputs.IndexOf(imitatedFromOutput);

                    if (im.mTemporaryCloneId == -1)
                    {
                        // Outside.
                        myInput.FromOutput = imitatedFromInput.FromOutput;
                    }
                    else
                    {
                        // Inside a group.
                        BaseModule mm = manager._GetModuleByTemporaryId(im.mTemporaryCloneId, im);

                        if (mm == im)
                        {
                            throw new Exception();
                        }

                        myInput.FromOutput = mm.Outputs[outputIndex];
                    }
                }
            }
        }

        private static void GenerateTemporaryIDs(BaseModule module1, BaseModule module2)
        {
            if (module1 == null)
            {
                throw new ArgumentNullException(nameof(module1));
            }
            if (module2 == null)
            {
                throw new ArgumentNullException(nameof(module2));
            }
            if (module1 == module2)
            {
                throw new Exception("Can not generate temporary id with the same module.");
            }
            if (module1.GetType() != module2.GetType())
            {
                throw new Exception("Can not generate temporary id with a module that is another type.");
            }

            module1.mTemporaryCloneId = module2.mTemporaryCloneId = mTemporaryCloneIdNext++;

            // If this module is a group.
            if (module1 is GroupModule)
            {
                GroupModule groupModule1 = module1 as GroupModule;
                GroupModule groupModule2 = module2 as GroupModule;

                for (int i = 0; i < groupModule1.SubModules.Count; i++)
                {
                    BaseModule m1 = groupModule1.SubModules[i];
                    BaseModule m2 = groupModule2.SubModules[i];
                    GenerateTemporaryIDs(m1, m2);
                }
            }
        }

        public bool CanSwitchType(IOType type)
        {
            return CanSwitchType(new HashSet<ModuleIO>(), type);
        }

        private bool CanSwitchType(HashSet<ModuleIO> fromIOs, IOType type)
        {
            if (!(type == IOType.Audio || type == IOType.Waveform || type == IOType.AudioOrWaveform))
            {
                throw new Exception("Only type of Audio and Waveform can be switched");
            }
            
            // Check all inputs/outputs if there is a IOType.AudioAndWaveForm
            foreach (var input in mInputs)
            {
                if (!fromIOs.Contains(input))
                {
                    fromIOs.Add(input);
                    if (!ModuleInputOutputCanBeSwitched(fromIOs, input, type))
                    {
                        return false;
                    }
                }
            }
            foreach (var output in mOutputs)
            {
                if (!fromIOs.Contains(output))
                {
                    fromIOs.Add(output);
                    if (!ModuleInputOutputCanBeSwitched(fromIOs, output, type))
                    {
                        return false;
                    }
                }
            }
            
            return true;
        }

        private bool ModuleInputOutputCanBeSwitched(HashSet<ModuleIO> fromIOs, ModuleIO inputOutput, IOType type)
        {
            if (type == IOType.Midi)
            {
                throw new Exception("Type of Midi is not supported.");
            }

            if (inputOutput.Type == IOType.AudioOrWaveform)
            {
                if (inputOutput.IsConnected)
                {
                    if (type == inputOutput.SwitchedType ||
                        inputOutput.SwitchedType == IOType.AudioOrWaveform) // Needs to switch.
                    {
                        // Connected input is the same type.

                        bool innerCanSwitch = inputOutput.Module.CanSwitchType(fromIOs, type);

                        if (!innerCanSwitch)
                        {
                            return false;
                        }
                    }
                    else
                    {
                        return false;
                    }
                }
            }
            else if (inputOutput.ActualType != type)
            {
                return false;
            }

            return true;
        }

        private void ClearAllTemporaryIDs()
        {
            // If this module is a group.
            if (this is GroupModule)
            {
                GroupModule group = this as GroupModule;

                foreach (var subModule in group.SubModules)
                {
                    subModule.ClearAllTemporaryIDs();
                }
            }

            mTemporaryCloneId = -1;
        }

        public void ClearAllConnections()
        {
            // If this module is a group.
            if (this is GroupModule)
            {
                GroupModule group = this as GroupModule;

                foreach (var subModule in group.SubModules)
                {
                    subModule.ClearAllConnections();
                }
            }

            foreach (var input in Inputs)
            {
                input.FromOutput = null;
            }
            foreach (var output in Outputs)
            {
                output.DisconnectAllInputs();
            }
        }

        public void ClearConnectedOutputs()
        {
            for (int i = Outputs.Count - 1; i >= 0; i--)
            {
                for (int j = Outputs[i].ConnectedInputs.Count - 1; j >= 0; j--)
                {
                    Outputs[i].RemoveInput(Outputs[i].ConnectedInputs[j]);
                }
            }
        }

        public void ClearConnectedInputs()
        {
            for (int i = Inputs.Count - 1; i >= 0; i--)
            {
                Inputs[i].FromOutput = null;
            }
        }

        public abstract void Process();

        public bool CompleteInputs()
        {
            // If no inputs at all, it is problably a generator. It is always ready.
            if (Inputs.Count == 0)
            {
                return true;
            }

            // If no inputs are connected and "has" inputs, it can be generated. (Problably a generator
            // with inputs that has no input connected)
            if (ConnectedInputCount() == 0)
            {
                return true;
            }

            // Okey, now we are here. Now it may be connected inputs. We check if they all has buffer ready.
            // If all is ready, then generate!
            bool allConnectedInputsAreaReady = true;
            foreach (var input in mInputs)
            {
                // Check if connected.
                if (input.IsConnected)
                {
#if DEBUG
                    if (!Manager.mAllModules.Contains(input.FromOutput.Module))
                    {
                        throw new Exception("Module do not exist. Can not process.");
                    }
#endif
                    // Check if connected output is ready.
                    if (!input.FromOutput.ReadyBuffer)
                    {
                        allConnectedInputsAreaReady = false;
                        break;
                    }
                }
            }
            if (allConnectedInputsAreaReady)
            {
                return true;
            }

            // Module has inputs connected. Try to wait until inputs has buffer ready.
            return false;
        }

        internal void SetBufferSize(int bufferSize)
        {
            foreach (var output in mOutputs)
            {
                output.SetBufferSize(bufferSize);
            }

            ChangeBufferSize(bufferSize);

            // Refresh parameters.
            foreach (var parameter in Parameters)
            {
                if (parameter is InputModuleParameter)
                {
                    (parameter as InputModuleParameter).OnBufferSizeChanged(bufferSize);
                }

                parameter.RawValue = parameter.RawValue;
            }
        }

        internal void SetBufferSizeChanged(int bufferSize)
        {
            BufferSizeChanged(bufferSize);
        }
        
        public bool NotCompleteButHasSomeWaitingInputs()
        {
            if (!CompleteInputs())
            {
                int connectedInputs = ConnectedInputCount();
                int connectedInputsAndReady = 0;

                // Check who is ready of all connected inputs.
                foreach (var input in mInputs)
                {
                    // Check if connected.
                    if (input.IsConnected)
                    {
                        // Check if connected output is ready.
                        if (!input.FromOutput.ReadyBuffer)
                        {
                            ++connectedInputsAndReady;
                        }
                    }
                }

                // No one was ready.
                if (connectedInputsAndReady == 0)
                {
                    return false;
                }

                // Ah okey, some inputs was ready!
                if (connectedInputsAndReady != connectedInputs)
                {
                    return true;
                }
            }

            return false;
        }

        public int ConnectedInputCount()
        {
            int count = 0;
            foreach (var input in Inputs)
            {
                if (input.IsConnected)
                {
                    ++count;
                }
            }

            return count;
        }

        public int ConnectedOutputCount()
        {
            int count = 0;
            foreach (var output in Outputs)
            {
                if (output.IsConnected)
                {
                    ++count;
                }
            }

            return count;
        }

        public string Name { get; set; }

        /// <summary>
        /// Get or set the process order index for the same generation position.
        /// </summary>
        public int OrderIndex { get; set; }

        /// <summary>
        /// Get the checked integer.
        /// </summary>
        public int Checked { get; internal set; }

        public ReadOnlyCollection<ModuleInput> Inputs { get; }

        public ReadOnlyCollection<ModuleOutput> Outputs { get; }

        /// <summary>
        /// Get the last processed time in milliseconds.
        /// </summary>
        public double LastProcessedTime { get; internal set; }

        public bool IsCircularConnected
        {
            get
            {
                foreach (var input in Inputs)
                {
                    if (input.IsCircularConnected)
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        /// <summary>
        /// Get the switched IOType if one or more input/output is typed as AudioOrWaveform.
        /// Otherwise it will return -1;
        /// </summary>
        public IOType SwitchedType
        {
            get
            {
                foreach (var input in mInputs)
                {
                    if (input.SwitchedType != NO_TYPE)
                    {
                        return input.SwitchedType;
                    }
                }

                foreach (var output in mOutputs)
                {
                    if (output.SwitchedType != NO_TYPE)
                    {
                        return output.SwitchedType;
                    }
                }

                return NO_TYPE;
            }
        }

        protected void AddInput(ModuleInput input)
        {
            if (mInputs.Contains(input))
            {
                throw new Exception("Input already exist.");
            }

            input.ID = mModuleIONextID++;

            mInputs.Add(input);
        }
        
        protected void RemoveInput(ModuleInput input)
        {
            if (!mInputs.Contains(input))
            {
                throw new Exception("Input do not exist.");
            }

            mInputs.Remove(input);

            // Disconnect the output.
            input.FromOutput = null;
        }
        
        protected void AddOutput(ModuleOutput output)
        {
            if (mOutputs.Contains(output))
            {
                throw new Exception("Output already exist.");
            }

            output.ID = mModuleIONextID++;

            mOutputs.Add(output);
        }
        
        protected void RemoveOutput(ModuleOutput output)
        {
            if (!mOutputs.Contains(output))
            {
                throw new Exception("Output do not exist.");
            }

            mOutputs.Remove(output);

            // Disconnect all inputs from this output.
            for (int i = output.ConnectedInputs.Count - 1; i >= 0; i--)
            {
                output.ConnectedInputs[i].FromOutput = null;
            }
        }
        
        public ModuleInput GetInput(int byID)
        {
            foreach (var input in mInputs)
            {
                if (input.ID == byID)
                {
                    return input;
                }
            }

            return null;
        }

        public ModuleOutput GetOutput(int byID)
        {
            foreach (var output in mOutputs)
            {
                if (output.ID == byID)
                {
                    return output;
                }
            }

            return null;
        }

        protected override void OnProcessEvents(List<MidiMessage> midiMessages)
        {
            throw new NotSupportedException("ProcessEvents(...) is not supported.");
        }

        public override string ToString()
        {
            return "ID: " + ID + " Module: " + Name;
        }

        protected static void Limit(ref double value, double min, double max)
        {
            value = value > max ? max : value < min ? min : value;
        }

        internal bool IsTopModule()
        {
            if (Manager.IsProtectedModule(this))
            {
                return false;
            }
            if (ParentModule != null && ParentModule.ParentModule == null)
            {
                return true;
            }

            return false;
        }

        internal bool IsRootModule()
        {
            return this == this.Manager;
        }
    }
}
